package com.meshnetics.gb.type.impl.zcl;

import com.meshnetics.gb.type.DataTypeID;
import com.meshnetics.gb.type.DataValue;
import com.meshnetics.gb.type.MandatoryFormatter;
import com.meshnetics.gb.type.impl.ByteUtils;
import com.meshnetics.gb.type.impl.StringUtils;

import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;

/**
 * WRITE DESCRIPTION HERE
 *
 * @author AKovylin
 */
public class TypeStr extends AbstractDataType {
    private static final String TYPE_CLASS_STRING = "String";

    public static final TypeStr STRING_OCTET = new TypeStr(
            new String[]{"ostr"}, new DataTypeID((byte) 0x41, TYPE_CLASS_STRING, "Octet string"), 1
    );
    public static final TypeStr STRING_CHARACTER = new TypeStr(
            new String[]{"cstr"}, new DataTypeID((byte) 0x42, TYPE_CLASS_STRING, "Character string"), 1
    );
    public static final TypeStr STRING_LONG_OCTET = new TypeStr(
            new String[]{"lostr"}, new DataTypeID((byte) 0x43, TYPE_CLASS_STRING, "Long octet string"), 2
    );
    public static final TypeStr STRING_LONG_CHARACTER = new TypeStr(
            new String[]{"lcstr"}, new DataTypeID((byte) 0x44, TYPE_CLASS_STRING, "Long character string"), 2
    );

    private final int lengthFieldSize;
    private final int invalidLength;
    private final StringDataMandatoryFormatter formatter = new StringDataMandatoryFormatter(this);

    protected TypeStr(String[] aliases, DataTypeID dataTypeId, int lengthFieldSize) {
        super(aliases, dataTypeId);
        if (lengthFieldSize < 1) {
            throw new IllegalArgumentException("invalid lengthFieldSize " + lengthFieldSize);
        }
        if (lengthFieldSize > 4) {
            throw new IllegalArgumentException("lengthFieldSize is too big");
        }
        this.lengthFieldSize = lengthFieldSize;
        this.invalidLength = (int) Math.pow(256, lengthFieldSize) - 1;
    }

    public int getLengthFieldSize() {
        return lengthFieldSize;
    }

    int getInvalidLength() {
        return invalidLength;
    }

    public DataValue fromObject(Object value) {
        if (value instanceof byte[]) {
            byte[] bytes = (byte[]) value;
            if (bytes.length > invalidLength) {
                throw new IllegalArgumentException("string too long");
            }
            return new Str(this, bytes.length, bytes.length == 0 ? null : (byte[]) bytes.clone());
        } else if (value instanceof String) {
            try {
                byte[] bytes = ((String) value).getBytes("UTF-8");
                return new Str(this, bytes.length, bytes.length == 0 ? null : bytes);
            } catch (UnsupportedEncodingException e) {
                throw new IllegalArgumentException("unsupported encoding", e);
            }
        } else {
            throw new IllegalArgumentException("byte array expected");
        }
    }

    public DataValue valueOf(byte[] value) {
        if (value == null) {
            throw new NullPointerException("value is null");
        }
        if (value.length < lengthFieldSize) {
            throw new IllegalArgumentException("byte array too small, minimum " + lengthFieldSize + " bytes required");
        }
        int length = readLength(value, 0, lengthFieldSize);
        if (length != invalidLength && length < 0) {
            throw new IllegalArgumentException("invalid length " + length);
        }
        byte[] innerValue = (length == 0 || length == invalidLength) ? null : new byte[length];
        if (innerValue != null) {
            if (value.length != lengthFieldSize + length) {
                throw new IllegalArgumentException("invalid string");
            }
            System.arraycopy(value, lengthFieldSize, innerValue, 0, length);
        } else if (value.length != lengthFieldSize) { // must be no bytes anymore!
            throw new IllegalArgumentException("invalid string");
        }
        return new Str(this, length, innerValue);
    }

    public DataValue deserialize(ByteBuffer bb) {
        ByteUtils.ensureNotNull(bb);
        ByteUtils.ensureCapacity(bb, lengthFieldSize);
        byte[] length = new byte[lengthFieldSize];
        ByteUtils.readFromByteBuffer(bb, length);
        int ilength = readLength(length, 0, length.length);
        if (ilength != invalidLength && ilength < 0) {
            throw new IllegalArgumentException("invalid length " + ilength);
        }
        byte[] value = (ilength == 0 || ilength == invalidLength) ? null : new byte[ilength];
        if (value != null) {
            ByteUtils.ensureCapacity(bb, ilength, ilength + lengthFieldSize);
            ByteUtils.readFromByteBuffer(bb, value);
        }
        return new Str(this, ilength, value);
    }

    // this method can't read "invalid" length of bytes!
    public DataValue deserialize(ByteBuffer bb, int length) {
        return deserialize(bb, length, true);
    }

    public DataValue deserialize(ByteBuffer bb, int length, boolean useBufferOrder) {
        ByteUtils.ensureNotNull(bb);
        // "invalid" length can't be specified here
        if (length < 0 || length >= invalidLength) {
            throw new IllegalArgumentException("length out of range");
        }
        ByteUtils.ensureCapacity(bb, length);
        byte[] value = (length == 0 || length == invalidLength) ? null : new byte[length];
        if (value != null) {
            if (useBufferOrder) {
                ByteUtils.readFromByteBuffer(bb, value);
            } else {
                bb.get(value);
            }
        }
        return new Str(this, length, value);
    }

    private int readLength(byte[] length, int offset, int count) {
        int ilength = 0;
        for (int i = 0; i < count; i++) {
            ilength = (ilength << 8) + (length[offset + i] & 0xff);
        }
        return ilength;
    }

    public MandatoryFormatter getMandatoryFormatter() {
        return formatter;
    }

    protected class StringDataMandatoryFormatter extends AbstractMandatoryFormatter {
        public StringDataMandatoryFormatter(TypeStr dataType) {
            super(dataType);
        }

        public DataValue valueOf(String serializedValue) {
            if (serializedValue == null) {
                throw new NullPointerException("serializedValue is null");
            }
            // 2 hex chars per byte
            if (serializedValue.length() < TypeStr.this.lengthFieldSize * 2) {
                throw new IllegalArgumentException("serializedValue must contain atleast " +
                        TypeStr.this.lengthFieldSize + " characters");
            }
            byte[] lengthField =
                    StringUtils.recoverBytesFromHexString(serializedValue, 0, TypeStr.this.lengthFieldSize * 2);
            int length = readLength(lengthField, 0, lengthField.length);
            if (length != invalidLength && length < 0) {
                throw new IllegalArgumentException("invalid length " + length);
            }
            byte[] innerValue;
            if (length == 0 || length == invalidLength) {
                if (serializedValue.length() != TypeStr.this.lengthFieldSize * 2) {
                    throw new IllegalArgumentException("invalid string");
                }
                innerValue = null;
            } else {
                if (serializedValue.length() != (TypeStr.this.lengthFieldSize + length) * 2) {
                    throw new IllegalArgumentException("invalid string");
                }
                innerValue = StringUtils.recoverBytesFromHexString(serializedValue,
                        TypeStr.this.lengthFieldSize * 2, length * 2);
            }
            return new Str(TypeStr.this, length, innerValue);
        }

        public String formatSerializedString(DataValue value) {
            checkValue(value);
            return StringUtils.toHexString(value.bytesValue());
        }
    }
}
